diff options
| author | joonhoekim <26rote@gmail.com> | 2025-07-23 06:06:27 +0000 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-07-23 06:06:27 +0000 |
| commit | f9bfc82880212e1a13f6bbb28ecfc87b89346f26 (patch) | |
| tree | ee792f340ebfa7eaf30d2e79f99f41213e5c5cf3 /app/[lng]/evcp/(evcp)/menu-access-dept/_components/department-menu-access-manager.tsx | |
| parent | edc0eabc8f5fc44408c28023ca155bd73ddf8183 (diff) | |
(김준회) 메뉴접근제어(부서별) 메뉴 구현
Diffstat (limited to 'app/[lng]/evcp/(evcp)/menu-access-dept/_components/department-menu-access-manager.tsx')
| -rw-r--r-- | app/[lng]/evcp/(evcp)/menu-access-dept/_components/department-menu-access-manager.tsx | 297 |
1 files changed, 297 insertions, 0 deletions
diff --git a/app/[lng]/evcp/(evcp)/menu-access-dept/_components/department-menu-access-manager.tsx b/app/[lng]/evcp/(evcp)/menu-access-dept/_components/department-menu-access-manager.tsx new file mode 100644 index 00000000..bf43e7a9 --- /dev/null +++ b/app/[lng]/evcp/(evcp)/menu-access-dept/_components/department-menu-access-manager.tsx @@ -0,0 +1,297 @@ +"use client"; + +import * as React from "react"; +import { useState, useTransition, useEffect } from "react"; +import { Settings, Plus, Users } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { toast } from "sonner"; +import { DepartmentTreeView } from "./department-tree-view"; +import { DepartmentDomainAssignmentDialog } from "./department-domain-assignment-dialog"; +import { + type DepartmentNode +} from "@/lib/users/knox-service"; +import { + assignDomainToDepartments, + getDepartmentDomainAssignments, + type UserDomain +} from "@/lib/users/department-domain/service"; +import { DOMAIN_OPTIONS } from "./domain-constants"; + +interface DepartmentMenuAccessManagerProps { + departmentsPromise: Promise<DepartmentNode[]>; + companyInfo: { code: string; name: string }; +} + +interface DepartmentAssignment { + id: number; + departmentCode: string; + departmentName: string; + assignedDomain: string; + description?: string | null; +} + +export function DepartmentMenuAccessManager({ + departmentsPromise, + companyInfo +}: DepartmentMenuAccessManagerProps) { + const [departments, setDepartments] = useState<DepartmentNode[]>([]); + const [selectedDepartments, setSelectedDepartments] = useState<string[]>([]); + const [assignments, setAssignments] = useState<DepartmentAssignment[]>([]); + const [isDialogOpen, setIsDialogOpen] = useState(false); + const [isPending, startTransition] = useTransition(); + const [isDepartmentsLoading, setIsDepartmentsLoading] = useState(true); + const [isAssignmentsLoading, setIsAssignmentsLoading] = useState(true); + + // Promise를 해결하여 부서 데이터 로드 + useEffect(() => { + const loadDepartments = async () => { + setIsDepartmentsLoading(true); + try { + const departmentTree = await departmentsPromise; + setDepartments(departmentTree); + } catch (error) { + console.error("부서 트리 로드 실패:", error); + toast.error("부서 정보를 불러오는데 실패했습니다."); + setDepartments([]); + } finally { + setIsDepartmentsLoading(false); + } + }; + + loadDepartments(); + }, [departmentsPromise]); + + // 기존 할당 정보 로드 + useEffect(() => { + const loadAssignments = async () => { + setIsAssignmentsLoading(true); + try { + const assignmentData = await getDepartmentDomainAssignments(); + setAssignments(assignmentData as DepartmentAssignment[]); + } catch (error) { + console.error("할당 정보 로드 실패:", error); + toast.error("할당 정보를 불러오는데 실패했습니다."); + setAssignments([]); + } finally { + setIsAssignmentsLoading(false); + } + }; + + loadAssignments(); + }, []); + + // 선택된 부서들의 정보 가져오기 + const getSelectedDepartmentInfo = React.useCallback(() => { + const findDepartment = (nodes: DepartmentNode[], code: string): DepartmentNode | null => { + for (const node of nodes) { + if (node.departmentCode === code) { + return node; + } + const found = findDepartment(node.children, code); + if (found) return found; + } + return null; + }; + + return selectedDepartments + .map(code => findDepartment(departments, code)) + .filter(Boolean) as DepartmentNode[]; + }, [departments, selectedDepartments]); + + // 도메인 할당 처리 + const handleDomainAssign = async (assignmentData: { + departmentCodes: string[]; + domain: string; + description?: string; + }) => { + // 선택된 부서들의 이름 매핑 생성 + const departmentNames: Record<string, string> = {}; + const collectDepartmentNames = (nodes: DepartmentNode[]) => { + nodes.forEach(node => { + if (assignmentData.departmentCodes.includes(node.departmentCode)) { + departmentNames[node.departmentCode] = node.departmentName || node.departmentCode; + } + collectDepartmentNames(node.children); + }); + }; + collectDepartmentNames(departments); + + startTransition(async () => { + try { + const result = await assignDomainToDepartments({ + departmentCodes: assignmentData.departmentCodes, + domain: assignmentData.domain as UserDomain, + description: assignmentData.description, + departmentNames, + }); + + if (result.success) { + toast.success(result.message); + setSelectedDepartments([]); + + // 할당 정보 새로고침 + try { + const updatedAssignments = await getDepartmentDomainAssignments(); + setAssignments(updatedAssignments as DepartmentAssignment[]); + } catch (error) { + console.error("할당 정보 새로고침 실패:", error); + } + } else { + toast.error(result.message); + } + } catch (error) { + console.error("도메인 할당 실패:", error); + toast.error("도메인 할당 중 오류가 발생했습니다."); + } + }); + }; + + const canAssign = selectedDepartments.length > 0; + const selectedDepartmentInfo = getSelectedDepartmentInfo(); + + const isLoading = isDepartmentsLoading || isAssignmentsLoading; + + return ( + <div className="grid grid-cols-1 lg:grid-cols-3 gap-6"> + {/* 왼쪽: 조직도 트리 */} + <div className="lg:col-span-2"> + <Card> + <CardHeader> + <CardTitle className="flex items-center gap-2"> + <Settings className="h-5 w-5" /> + 조직도 - {companyInfo.name} + </CardTitle> + <CardDescription> + 부서를 선택하여 도메인을 할당하세요. 상위 부서 선택 시 하위 부서들도 자동으로 포함됩니다. + </CardDescription> + </CardHeader> + <CardContent className="p-0"> + {isLoading ? ( + <div className="flex items-center justify-center h-[80vh]"> + <div className="text-center"> + <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto mb-4"></div> + <p className="text-muted-foreground">조직도를 불러오는 중...</p> + </div> + </div> + ) : ( + <DepartmentTreeView + departments={departments} + selectedDepartments={selectedDepartments} + onSelectionChange={setSelectedDepartments} + assignments={assignments} + /> + )} + </CardContent> + </Card> + </div> + + {/* 오른쪽: 선택된 부서 정보 및 할당 버튼 */} + <div className="space-y-6"> + {/* 선택된 부서 정보 */} + <Card> + <CardHeader> + <CardTitle className="flex items-center gap-2"> + <Users className="h-5 w-5" /> + 선택된 부서 + </CardTitle> + <CardDescription> + {selectedDepartments.length}개 부서가 선택되었습니다 + </CardDescription> + </CardHeader> + <CardContent> + {selectedDepartmentInfo.length === 0 ? ( + <div className="text-center py-4 text-muted-foreground"> + 부서를 선택해주세요 + </div> + ) : ( + <div className="space-y-2 max-h-60 overflow-y-auto"> + {selectedDepartmentInfo.map((dept) => { + const assignment = assignments.find(a => a.departmentCode === dept.departmentCode); + return ( + <div + key={dept.departmentCode} + className="flex items-center justify-between p-2 bg-accent/20 rounded-md" + > + <div className="min-w-0"> + <div className="font-medium truncate"> + {dept.departmentName || dept.departmentCode} + </div> + <div className="text-xs text-muted-foreground"> + {dept.departmentCode} + </div> + </div> + {assignment && ( + <Badge variant="outline" className="text-xs shrink-0"> + {assignment.assignedDomain} + </Badge> + )} + </div> + ); + })} + </div> + )} + </CardContent> + </Card> + + {/* 도메인 할당 버튼 */} + <Card> + <CardHeader> + <CardTitle className="text-lg">도메인 할당</CardTitle> + <CardDescription> + 선택된 부서들에 도메인을 할당합니다 + </CardDescription> + </CardHeader> + <CardContent> + <Button + onClick={() => setIsDialogOpen(true)} + disabled={!canAssign || isPending} + size="lg" + className="w-full" + > + <Plus className="mr-2 h-4 w-4" /> + 도메인 할당 ({selectedDepartments.length}개 부서) + </Button> + + {canAssign && ( + <div className="mt-3 text-sm text-muted-foreground"> + 상위 부서를 선택한 경우 하위 부서들도 자동으로 동일한 도메인이 할당됩니다. + </div> + )} + </CardContent> + </Card> + + {/* 범례 */} + <Card> + <CardHeader> + <CardTitle className="text-lg">도메인 범례</CardTitle> + </CardHeader> + <CardContent> + <div className="grid grid-cols-1 gap-2 text-sm"> + {DOMAIN_OPTIONS.map((option) => ( + <div key={option.value} className="flex items-center gap-2"> + <Badge className={option.color}> + {option.value} + </Badge> + <span>{option.description}</span> + </div> + ))} + </div> + </CardContent> + </Card> + </div> + + {/* 도메인 할당 다이얼로그 */} + <DepartmentDomainAssignmentDialog + open={isDialogOpen} + onOpenChange={setIsDialogOpen} + selectedDepartments={selectedDepartments} + departments={departments} + companyInfo={companyInfo} + onAssign={handleDomainAssign} + isLoading={isPending} + /> + </div> + ); +}
\ No newline at end of file |
